﻿
using EmperialApps.WeatherSpark.Data;
using Microsoft.WindowsAzure.Storage;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;

namespace EmperialApps.WeatherSpark.Service.Internal {

    /// <summary>Acts as an aggregator for the storage and retrieval of forecasts and forecast images.</summary>
    internal sealed class ForecastAggregator : IForecastStore {

        /// <summary>Initializes a new instance of the <see cref="ForecastBlobStore"/> class.</summary>
        public ForecastAggregator( CloudStorageAccount account, ForecastInfoTableStore forecastInfo, params ForecastSource[] sources ) {
            this._forecastInfo = forecastInfo;
            this._sources = sources;

            // Get forecast stores and image generators for each container.
            int length = sources.Length;
            this._stores = new ForecastBlobStore[length];
            this._imageGenerators = new ForecastImageGenerator[length];
            for( int i = 0; i < length; ++i ) {
                ForecastSource source = sources[i];
                TraceEventType.Verbose.Trace( "Initializing {0} forecast store and image generator...", source );

                var store = new ForecastBlobStore( account, forecastInfo, source );
                store.DownloadCompleted += this.OnDownloadCompleted;
                this._stores[i] = store;

                var generator = new ForecastImageGenerator( account, source );
                this._imageGenerators[i] = generator;
            }
        }


        /// <summary>Ends image creation.</summary>
        public void Stop( ) {
            foreach( var generator in this._imageGenerators )
                generator.Stop( );
        }


        /// <summary>Occurs when a forecast is stored.</summary>
        public event EventHandler<ForecastEventArgs> ForecastStored = delegate { };

        /// <summary>Retrieves the storage location of the specified forecast, if it exists.</summary>
        public string GetUrl( Forecast forecast ) {
            return forecast != null
                 ? this.GetUrl( forecast.Location, null )
                 : "";
        }

        /// <summary>Retrieves the storage location of the specified forecast, if it exists.</summary>
        public string GetUrl( Coordinate location, string preferredUrl ) {
            return this.GetInfoUrl( location, preferredUrl )
                ?? this.GetBlobUrl( location );
        }

        /// <summary>Checks whether other sources are available to act as a fallback for the failing source.</summary>
        public void Fallback( ForecastInfo info, ForecastSource failingSource ) {
            int failingIndex = Array.IndexOf( this._sources, failingSource );
            object state = Pair.Create( -1, failingIndex );
            FindFallback( info, state, null );
        }

        private void FindFallback( ForecastInfo info, object state, Action<ForecastInfo> initializeFallback ) {
            int index, failingIndex;
            state.TryGetValues( out index, out failingIndex );

            // If source can work as a fallback, initialize it.
            if( initializeFallback != null ) {
                var source = this._sources[index];
                var fallbackStore = this._stores[index];
                Coordinate location = info.GetLocation( );

                TraceEventType.Information.Trace( "Found {0} fallback for {1}", source, info );
                var currentInfo = this._forecastInfo.Get( location );
                initializeFallback( currentInfo );
                this._forecastInfo.Update( currentInfo );

                if( this._pendingRedirects.TryAdd( location, failingIndex ) )
                    fallbackStore.BeginDownload( location );
                else
                    TraceEventType.Warning.Trace( "Could not save redirect for " + location );
            }
            // Otherwise, check next source (ignoring the original failing source).
            else {
                do { ++index; }
                while( index < this._sources.Length && index == failingIndex );

                if( index < this._sources.Length ) {
                    var source = this._sources[index];
                    state = Pair.Create( index, failingIndex );

                    TraceEventType.Verbose.Trace( "Testing {0} fallback for {1}", source, info );
                    source.TryFallback( info, state, FindFallback );
                }
            }
        }


        /// <inheritdoc/>
        public bool Save( ForecastEventArgs e ) {
            var sourcedArgs = (SourcedForecastEventArgs)e;
            var store = this._stores[sourcedArgs.SourceIndex];
            var generator = this._imageGenerators[sourcedArgs.SourceIndex];

            // Save forecast and queue image.
            bool success = store.Save( e );
            e.Try( generator.Queue, false );

            // Notify subscribers of update.
            this.ForecastStored( this, e );
            return success;
        }

        /// <inheritdoc/>
        public Forecast Load( Coordinate location, object context ) {
            Forecast forecast;

            var sourcedArgs = context as SourcedForecastEventArgs;
            if( sourcedArgs != null ) {
                var store = this._stores[sourcedArgs.SourceIndex];
                forecast = store.Load( location, context );
            }
            else {
                forecast = null;
                for( int i = 0; forecast == null && i < this._stores.Length; ++i ) {
                    var store = this._stores[i];
                    forecast = store.Load( location, context );
                }
            }

            return forecast;
        }

        /// <inheritdoc/>
        public IEnumerable<Coordinate> GetStoredLocations( ) {
            var storedLocations = new HashSet<Coordinate>( );
            foreach( var store in this._stores )
                storedLocations.UnionWith( store.GetStoredLocations( ) );

            return storedLocations;
        }


        /// <inheritdoc/>
        public event EventHandler<ForecastEventArgs> DownloadCompleted;

        /// <inheritdoc/>
        public void BeginDownload( Coordinate location ) {
            foreach( var store in this._stores )
                store.BeginDownload( location );
        }


        #region Private Members

        private readonly ConcurrentDictionary<Coordinate, int> _pendingRedirects = new ConcurrentDictionary<Coordinate, int>( );
        private readonly ForecastInfoTableStore _forecastInfo;
        private readonly ForecastImageGenerator[] _imageGenerators;
        private readonly ForecastBlobStore[] _stores;
        private readonly ForecastSource[] _sources;


        private string GetInfoUrl( Coordinate location, string preferredUrl ) {
            ForecastInfo info;
            try {
                info = this._forecastInfo.Get( location );
            }
            catch( StorageException ex ) {
                info = ex.IsTimeoutException( )
                     ? null
                     : this._forecastInfo.Get( location );
            }

            // If new forecast is requested with a preferred URL, save the preferred value.
            if( info == null && ForecastSource.SaveForecastSourceUrl( location, preferredUrl, out info ) )
                this._forecastInfo.Add( info );

            return info != null
                 ? info.ForecastUrl
                 : null;
        }

        private string GetBlobUrl( Coordinate location ) {
            string url = null;
            for( int i = 0; url == null && i < this._stores.Length; ++i ) {
                var store = this._stores[i];
                url = store.GetBlobUrl( location );
            }

            return url;
        }

        private void OnDownloadCompleted( object sender, ForecastEventArgs e ) {
            var store = (ForecastBlobStore)sender;

            // Process any pending redirects.
            int failingStoreIndex;
            if( this._pendingRedirects.TryRemove( e.Location, out failingStoreIndex ) )
                this.ProcessRedirect( e, store, failingStoreIndex );

            // Notify listeners of download.
            var handler = this.DownloadCompleted;
            if( handler == null )
                return;

            int sourceIndex = Array.IndexOf( this._stores, store );
            var sourcedArgs = new SourcedForecastEventArgs( e, sourceIndex );
            handler( this, sourcedArgs );
        }

        private void ProcessRedirect( ForecastEventArgs e, ForecastBlobStore store, int failingStoreIndex ) {
            Coordinate location = e.Location;
            if( store.Load( location, null ) != null || store.Save( e ) ) {
                var info = this._forecastInfo.Get( location );
                string fallbackUrl = store.GetBlobUrl( e.Forecast.Location );
                Debug.Assert( !string.IsNullOrEmpty( fallbackUrl ), "Successfully saved forecast event did not result in valid URL: " + e );

                TraceEventType.Verbose.Trace( "Saving fallback URL {0} for {1}", fallbackUrl, info );
                Forecast redirect = e.Forecast.Redirect( fallbackUrl );
                var failingStore = this._stores[failingStoreIndex];
                failingStore.Save( new ForecastEventArgs( location, redirect ) );
            }
            else {
                var info = this._forecastInfo.Get( location );
                TraceEventType.Warning.Trace( "Could not download fallback forecast for " + info );
            }
        }


        private sealed class SourcedForecastEventArgs : ForecastEventArgs {
            public readonly int SourceIndex;

            public SourcedForecastEventArgs( ForecastEventArgs e, int sourceIndex )
                : this( e.Location, e.Forecast, e.Error, sourceIndex ) { }

            private SourcedForecastEventArgs( Coordinate coordinate, Forecast forecast, Exception exception, int sourceIndex )
                : base( coordinate, forecast, exception ) {
                this.SourceIndex = sourceIndex;
            }

            public override ForecastEventArgs UpdateForecast( Forecast forecast ) {
                return new SourcedForecastEventArgs( this.Location, forecast, this.Error, this.SourceIndex );
            }

            public override string ToString( ) {
                return "[" + this.SourceIndex + "] " + base.ToString( );
            }
        }

        #endregion
    }

}
